from general_utilities import *
# from entities import * - does not work?
from conditions import *
from pathfinding import *
from notifications import *
from glyphs import right_down_to_angle

from math import ceil
import pygame.time

A_NOTHING = 0
A_WALK = 1        # fill in target if displacing them
A_USE_PORTAL = 3  # fill in target if displacing them
A_USE_EXIT = 4
A_HIT = 2        
A_SHOT = 11
A_ROCK = 11.5
A_SMITE = 5
A_SUPER_SMITE = 5.5
A_CLEAVE = 6 # single victim, will be repeated multiple times
A_SILENT_CLEAVE = 28 # all but the first and last using this action
A_JAB = 7
A_JUMP = 13
A_BLINK = 17
A_TELEPORTATION = 18
A_TELEPORT_LONG = 18.5
A_ALL_OUT_DEFENSE = 8
A_HEALING = 25
A_DETECT_ENEMIES = 27
A_CLAIRVOYANCE = 22
A_FIREBOLT = 19
A_LIGHTNING_BOLT = 21
A_CONFUSION = 20
A_EXORCISM = 23
A_CIRCLE_OF_PROTECTION = 24 # not fully implemented for monsters currently
A_SNEAK = 15 # not fully implemented for monsters currently
A_DODGE = 16
A_ADMONISHMENT = 26 
A_FEAR = 26.5
A_ZAP = 29
A_FIREBREATH = 30 
A_BIG_FIREBREATH = 30.5
A_FIRERES = 31
A_CIRCLE_OF_FIRE = 32
A_BLESSING = 33
A_INVISIBILITY = 34
A_STEAL_ITEM_SPELL = 35
A_WINDBLAST = 36
A_PICKLOCK = 37
A_JAMLOCK = 38

action_mana_costs = {}
action_mana_costs[A_BLINK] = 6
action_mana_costs[A_TELEPORTATION] = 14
action_mana_costs[A_TELEPORT_LONG] = 20
action_mana_costs[A_HEALING] = 12
action_mana_costs[A_FIREBOLT] = 8
action_mana_costs[A_CONFUSION] = 6
action_mana_costs[A_LIGHTNING_BOLT] = 12
action_mana_costs[A_WINDBLAST] = 10
action_mana_costs[A_CLAIRVOYANCE] = 4
action_mana_costs[A_EXORCISM] = 10
action_mana_costs[A_CIRCLE_OF_PROTECTION] = 6
action_mana_costs[A_ADMONISHMENT] = 6
action_mana_costs[A_DETECT_ENEMIES] = 4
action_mana_costs[A_ZAP] = 2
action_mana_costs[A_CIRCLE_OF_FIRE] = 6
action_mana_costs[A_FIRERES] = 6
action_mana_costs[A_FEAR] = 2
action_mana_costs[A_STEAL_ITEM_SPELL] = 4

class Action:
  def __init__(self,entity,type,target_pos,target):
    self.entity = entity # the actor
    self.type = type
    self.target_pos = target_pos
    self.target = target # the 'victim' entity (or may be None) (or an [object])
    self.energy_multiplier = 1.0 # can change this later  

  def expend_energy(self,base):
    self.entity.spend_energy(base*self.energy_multiplier)
  
  def resolve(self,d):
    cost = action_mana_costs.get(self.type,0)
    if self.entity.is_character:
      cost = cost*2
    if cost>0:
      if self.entity.mp < cost:
        if self.entity.is_character:
          n = Notification(self.entity.pos)
          n.set_noise(1,SD_CAST,10,self.entity,1,1)
          n.set_message(self.entity.name+" has run out of magical power...",0)
          n.set_sound("cancel1")
          n.play(d)
        self.expend_energy(1)    
        return
      else:
        self.entity.mp = self.entity.mp - cost    
    if self.type == A_WALK:
      # first round of validity checking
      if not d.l.valid(self.target_pos) or not adjacent(d,self.target_pos,self.entity.pos):
        if self.entity.is_character:
          d.clear_action_stack()
        return 
      if not d.l.tile_at(self.target_pos).info().walkable(self.entity):
        if self.entity.is_character:
          self.expend_energy(1)
          d.clear_action_stack()
        return         
      if self.entity.info().immobile:
        return
      # second round of validity checking
      occupant = d.get_occupant(self.target_pos)
      if occupant is not None and ((not self.entity.can_push(d,occupant)) or self.entity.is_enemy(occupant)):
        if self.entity.is_character:
          d.clear_action_stack()
        else:
          self.expend_energy(0.5)
        return         
      if self.target==self.entity:
        self.target = None # common misspecification
      move_cost = self.entity.info().move_cost
      if d.l.tile_at(self.entity.pos).info().slow_without_webwalk and not self.entity.info().web_walk:
        move_cost = move_cost * 2.5
        if self.entity.is_character:
          display.send_message(sentencecase(self.entity.name + " is hindered by the webs!"))
      elif self.entity.is_character and len(d.get_entities_in_room(self.entity.pos))>1 and random.random()<0.01*self.entity.info().speed and self.entity.use_zap(7,3):
        if move_cost>=1.1:
          display.send_message("Your hero is running as fast as he can...")
        else:
          display.send_message("Your hero dashes quickly!")
        sound.play('evade2')
        move_cost = move_cost * 0.5
      if self.entity.is_character and move_cost > 0.9:
        wait(ms=int(200*(move_cost-0.9)))
      self.expend_energy(move_cost)
      self.entity.move(d,self.target_pos,self.target)
    elif self.type == A_JUMP:
      # first round of validity checking
      if not d.l.valid(self.target_pos) or not d.l.tile_at(self.target_pos).info().walkable(self.entity):
        if self.entity.is_character:
          d.clear_action_stack()
        return 
      # second round of validity checking
      jump_path = []
      dir = self.entity.pos.direction_towards(self.target_pos)
      this_pos = self.entity.pos.copy()
      while this_pos<>self.target_pos:
        dir = this_pos.direction_towards(self.target_pos)
        this_pos = get_pos_in_dir(d,this_pos,dir)
        jump_path.append(this_pos.copy())
      ok = 1
      for pos in jump_path:
        if not d.l.tile_at(pos).info().leapable:
          ok = 0
        occupant = d.get_occupant(pos)
        if occupant is not None:
          ok = 0
      if ok==0:
        if self.entity.is_character:
          d.clear_action_stack()
        return         
      move_cost = self.entity.info().move_cost
      if d.l.tile_at(self.entity.pos).info().slow_without_webwalk and not self.entity.info().web_walk:
        move_cost = move_cost * 2.5
        if self.entity.is_character:
          display.send_message(self.entity.name + " is hindered by the webs!")
      if self.entity.is_character and move_cost > 0.9:
        wait(ms=int(250*(move_cost-0.9)))
      self.expend_energy(move_cost*1.5)
      n = Notification(self.entity.pos)
      n.set_sound("bump1")
      n.play(d)
      self.entity.move(d,self.target_pos,None)      
    elif self.type == A_BLINK or self.type == A_TELEPORTATION:
      if self.type == A_BLINK:
        self.target_pos = self.entity.pos.copy()
      count = 0
      while count<=100:
        count = count + 1
        if d.l.valid(self.target_pos):
          if self.target_pos<>self.entity.pos:
            if d.l.tile_at(self.target_pos).info().walkable(self.entity):
              if not d.l.tile_at(self.target_pos).info().no_teleport:
                occupant = d.get_occupant(self.target_pos)
                if occupant is None:
                  n = Notification(self.entity.pos)
                  n.set_noise(1,SD_CAST,10,self.entity,1,1)
                  n.set_message(self.entity.name+" teleports!",0)
                  n.set_sound("teleport1")
                  n.play(d)
                  self.expend_energy(1)
                  self.entity.move(d,self.target_pos,None)
                  return
        self.target_pos.right = random.randint(0,d.l.room_at(self.target_pos).width-1)
        self.target_pos.down = random.randint(0,d.l.room_at(self.target_pos).height-1)         
    elif self.type == A_TELEPORT_LONG:
      self.target_pos = self.entity.pos.copy()
      count = 0
      while count<=100:
        count = count + 1
        if d.l.valid(self.target_pos):
          if self.target_pos<>self.entity.pos:
            if d.l.tile_at(self.target_pos).info().walkable(self.entity):
              if not d.l.tile_at(self.target_pos).info().no_teleport:
                occupant = d.get_occupant(self.target_pos)
                if occupant is None:
                  n = Notification(self.entity.pos)
                  n.set_noise(1,SD_CAST,10,self.entity,1,1)
                  n.set_message(self.entity.name+" casts a long-range teleport!",0)
                  n.set_sound("teleport1")
                  n.play(d)
                  self.expend_energy(1)
                  self.entity.move(d,self.target_pos,None)
                  return
        valid_room = 0
        self.target_pos.right = 0
        self.target_pos.down = 0
        while (not valid_room):
          self.target_pos.r_right = random.randint(0,d.l.level_at(self.target_pos).r_width-1)
          self.target_pos.r_down = random.randint(0,d.l.level_at(self.target_pos).r_height-1)
          if d.l.valid(self.target_pos):
            valid_room = 1
        self.target_pos.right = random.randint(0,d.l.room_at(self.target_pos).width-1)
        self.target_pos.down = random.randint(0,d.l.room_at(self.target_pos).height-1)         
    elif self.type == A_HIT or self.type == A_SMITE or self.type == A_SUPER_SMITE or self.type == A_CLEAVE or self.type == A_SILENT_CLEAVE or self.type == A_JAB:
      if not d.l.valid(self.target_pos) or not d.get_occupant(self.target_pos)==self.target:
        return 
      if self.type<>A_JAB and not adjacent(d,self.target_pos,self.entity.pos): 
        return
      if self.target.brain is not None:
        self.target.brain.disturbed = 1
      if self.type==A_SUPER_SMITE:
        self.expend_energy(1.5*self.entity.info().melee_cost)
      elif self.type==A_SMITE:
        self.expend_energy(1.2*self.entity.info().melee_cost)
      else:
        self.expend_energy(1*self.entity.info().melee_cost)
      dodge = 0
      if random.random() < self.entity.get_melee_hit_chance(d,self.target):
        hit = 1
        expected_damage = self.entity.info().avg_melee_damage
        zap_hit = 0
        if random.random()<0.3 and self.entity.use_zap(5,2):
          zap_hit = 1
          expected_damage = expected_damage * 1.5
        damage = random_positive_int_around(expected_damage,0.5)
      else:
        hit = 0
      if hit and ((self.target.has_condition(C_DODGING) and random.random()<0.5) or (self.target.is_character and random.random()<0.3 and self.target.use_zap(7,5))):
        dodge_to = d.nearby_unthreatened_space(self.target) 
        if dodge_to is not None:
          dodge = 1
          hit = 0
          self.target.move(d,dodge_to,None)
          move_cost = self.target.info().move_cost
          self.target.spend_energy(move_cost*0.75)
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_BATTLE,20,self.entity,1,0.7)
      if hit:
        n = Notification(self.entity.pos)
        n.set_sound("punch2")
        n.set_flash(self.target_pos,display.thump,0.2)
        punctuation = '.'
        if self.type==A_SMITE:
          verb = "smites"
          punctuation = '!'
          n.set_sound('089-Attack01')
          damage = damage*2
        elif (not self.entity.is_character and self.entity.info().knockback):
          verb = "bashes"
          punctuation = '!'
          n.set_sound('089-Attack01')
        elif self.type==A_SUPER_SMITE:
          verb = "SMITES"
          punctuation = '!'
          n.set_sound('089-Attack01')
          damage = damage*3
          n.set_flash(self.target_pos,display.bigthump,0.2)
        elif self.type==A_CLEAVE or self.type==A_SILENT_CLEAVE:
          verb = "cleaves"
          punctuation = '!'
          if self.type==A_SILENT_CLEAVE:
            n.set_sound(None)
          else:
            n.set_sound('strike10')
        elif self.type==A_JAB:
          verb = "jabs"
        elif zap_hit:
          verb = "smites"
          punctuation = '!'
          n.set_sound('089-Attack01')
        elif self.entity.is_character and self.entity.melee_weapon is None:
          verb = "punches"       
        elif not self.entity.is_character:
          verb = self.entity.info().melee_verb
          if verb=="claws":
            n.set_sound('strike11')
          else:
            n.set_sound("092-Attack04")
        else:
          verb = "hits"
        if self.target.vulnerable:
          verb = "sneakily " + verb
          damage = damage * 2
        if self.entity.is_character and self.target.info().dragon and self.entity.melee_weapon is not None and self.entity.melee_weapon.info().dragon_slayer:
          damage = damage * 2
        if (not self.entity.is_character) and self.entity.info().melee_burns:
          fireres_helps = self.entity.info().melee_burns # can be 1 or 0.5, depending on how much it helps
          n.set_sound('118-Fire02')
        else:
          fireres_helps = 0
        n.set_message(self.entity.name+' '+verb+' '+self.target.name+punctuation,0)
        n.sound_volume_mult = min(1.0,float(damage+1.0)/7)
        n.play(d)
        trivial = self.target.take_damage(d,damage,armor_helps=1,fire_resistance_helps=fireres_helps)
        if trivial:
          n = Notification(self.entity.pos)
          n.set_message(self.target.info().name + " shrugs it off.",0)
          n.play(d)
        if self.entity.is_character and self.entity.melee_weapon is not None:
          if self.entity.melee_weapon.info().regain_mp_on_kill and self.target.is_dead():
            self.entity.mp = min(self.entity.original_mp,self.entity.mp+int(0.1*self.entity.original_mp))
          elif self.entity.melee_weapon.info().admonish_on_hit and not self.target.is_dead() and random.random()<0.5:
            admonishment_action = Action(self.entity,A_ADMONISHMENT,self.target_pos,self.target)
            admonishment_action.resolve(d)
        if not self.target.is_dead() and not self.target.info().immune_to_poison:
          poison_level = 0
          if not self.entity.is_character and self.entity.info().poison_bite:
            poison_level = self.entity.info().poison_bite # can be 1 or 2
          elif self.entity.is_character and self.entity.info().poisoned_melee_weapon:
            poison_level = 1
          if poison_level>0:
            self.target.get_poisoned(d,poison_level)
        if not dodge:
            knockback_chance = 0.0
            if self.entity.is_character:
              if self.target.info().size==SZ_SMALL:
                knockback_chance = float(damage-6) / 10
              elif self.target.info().size==SZ_MEDIUM:
                knockback_chance = float(damage-9) / 20
            elif self.entity.info().knockback:
              if self.target.info().size==SZ_SMALL:
                knockback_chance = 0.8
              elif self.target.info().size==SZ_MEDIUM:
                knockback_chance = 0.5             
            if knockback_chance > 0 and random.random() < knockback_chance:
              try_for_knockback(d,self.target,self.entity.pos)
      else:
        n.set_sound("064-Swing03")
        if dodge:
          n.set_message(self.target.name+" dodges the attack of " + self.entity.name + "!",0)
        else:
          if self.target.has_condition(C_DEFENDING):
            verb = "is parried by"
            n.set_sound("sword_on_shield")
          else:
            verb = "misses"
            n.sound_volume_mult = 0.75
          n.set_message(self.entity.name+" "+verb+" " + self.target.name + ".",0)
        n.play(d)
    elif self.type == A_SHOT or self.type == A_ZAP or self.type == A_ROCK:
      if not d.l.valid(self.target_pos) or not d.get_occupant(self.target_pos)==self.target:
        return 
      if self.target.brain is not None:
        self.target.brain.disturbed = 1
      self.expend_energy(1*self.entity.info().missile_cost)
      hit_chance = self.entity.get_missile_hit_chance(d,self.target)
      use_zap = 0
      if self.entity.use_zap(5,2):
        hit_chance = hit_chance + 0.2
        use_zap = 1
      if random.random() < hit_chance:
        hit = 1
        armor_helps = 1
        magic_resistance_helps = 0
        fire_resistance_helps = 0
        if self.type == A_SHOT or self.type == A_ROCK:
          exp_damage = self.entity.info().avg_missile_damage
        elif self.type == A_ZAP:
          armor_helps = 0
          magic_resistance_helps = 1
          exp_damage = 7
        damage = random_positive_int_around(exp_damage,0.5)
        if self.entity.is_character and self.entity.missile_weapon is not None:
          if self.entity.missile_weapon.info().fire_arrows:
            fire_resistance_helps = 1
            damage = damage * 2
      else:
        hit = 0
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_BATTLE,10,self.entity,1,0.5)
      if hit:
        n.set_sound("ArrHit")
        punctuation = '.'
        if (self.type==A_ROCK) or (self.entity.is_character and self.entity.missile_weapon is None):
          verb = "throws a rock at"
          n.set_flash(self.target_pos,display.stone,0.2)
        elif self.type==A_ZAP:
          n.set_sound("shot3")   
          verb = "zaps"
          n.set_flash(self.target_pos,display.zap,0.2)
        else:
          verb = "shoots"
          n.set_flash(self.target_pos,display.arrow,0.2)
        if use_zap:
          verb = "quickly " + verb
        if fire_resistance_helps:
          n.set_flash(self.target_pos,display.firearrow,0.2)
        n.set_message(self.entity.name+' '+verb+' '+self.target.name+punctuation,0)
        n.play(d)
        trivial = self.target.take_damage(d,damage,armor_helps=armor_helps,magic_resistance_helps=magic_resistance_helps,fire_resistance_helps=fire_resistance_helps)
        if trivial:
          n = Notification(self.entity.pos)
          n.set_message(self.target.info().name + " shrugs it off.",0)
          n.play(d)
        if self.entity.is_character and self.type==A_ZAP and not self.target.is_dead() and random.random()<0.1:
          # something special might happen
          # could be confusion:
          difficulty = 10
          if self.target.make_magic_resistance_check(difficulty) or self.target.info().immune_to_enchantments:
            pass
          else:  
            self.target.add_condition(d,C_CONFUSED,2,6)
            n.set_message(self.target.name + " is confused!",0)
            n.set_flash(self.target_pos,display.confuse,0.2)
            n.set_sound("confuse") 
      else:
        n.set_sound("miss")     
        n.set_message(self.entity.name+" misses " + self.target.name + ".",0)
        n.sound_volume_mult = 0.75
        n.play(d)
    elif self.type == A_EXORCISM:
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_CAST,10,self.entity,1,1)      
      n.set_message(self.entity.name+" performs an exorcism!",0)
      n.set_sound("holy5")
      n.play(d)
      for m in d.get_entities_in_room(self.entity.pos):        
        if (not m.no_longer_here()) and m is not self.entity:
          if not(m.is_character) and m.info().undead:
            m.take_damage(d,20)
      self.expend_energy(1)        
    elif self.type == A_CIRCLE_OF_PROTECTION:
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_CAST,10,self.entity,1,1)      
      n.set_message(self.entity.name+" invokes a circle of protection!",0)
      n.set_sound("holy7")
      n.play(d)
      self.entity.add_condition(d,C_CIRCLE_OF_PROTECTION,10,10)
      self.expend_energy(1)
      display.redraw_tile(d,self.entity.pos,self.entity)
      display.show_glyphs(d)
      display.blit_map_to_screen()
    elif self.type == A_CIRCLE_OF_FIRE:
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_CAST,20,self.entity,1,1)      
      n.set_message(self.entity.name+" is surrounded by a ring of flames!",0)
      n.set_sound("119-Fire03")
      n.play(d)
      for down_right in d.shells[1]:
        this_pos = self.entity.pos.copy()
        this_pos.down = this_pos.down + down_right[0]
        this_pos.right = this_pos.right + down_right[1]
        if not d.l.valid(this_pos):
          continue
        this_tile = d.l.tile_at(this_pos)
        if this_tile.info().walkable(self.entity) and not this_tile.info().is_fiery and not this_tile.info().is_watery and not d.l.is_temporary_fire(this_pos):
          d.l.add_temporary_fire(this_pos)
          occupant = d.get_occupant(this_pos)
          display.redraw_tile(d,this_pos,occupant)
      display.blit_map_to_screen()
      self.expend_energy(1)
    elif self.type == A_FIRERES:
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_CAST,10,self.entity,1,1)      
      n.set_message(self.entity.name+" is resistant to fire!",0)
      n.set_sound("holy7")
      n.play(d)
      self.entity.add_condition(d,C_FIRERES_FROM_SPELL,40,30)
      self.expend_energy(1)
    elif self.type == A_PICKLOCK:
      locked_door = d.l.tile_at(self.target_pos)
      if locked_door.info().is_portal:
        d.l.room_at(self.target_pos).tiles[self.target_pos.down][self.target_pos.right] = Tile(T_DOOR,locked_door.wall_type,locked_door.floor_type)
        d.l.room_at(self.target_pos).tiles[self.target_pos.down][self.target_pos.right].to_pos = locked_door.to_pos
        other_side = d.l.tile_at(locked_door.to_pos)
        d.l.room_at(locked_door.to_pos).tiles[locked_door.to_pos.down][locked_door.to_pos.right] = Tile(T_DOOR,other_side.wall_type,other_side.floor_type)
        d.l.room_at(locked_door.to_pos).tiles[locked_door.to_pos.down][locked_door.to_pos.right].to_pos = other_side.to_pos
        d.room_network.nodes[(self.target_pos.level,self.target_pos.r_down,self.target_pos.r_right)] = None 
        d.room_network.nodes[(locked_door.to_pos.level,locked_door.to_pos.r_down,locked_door.to_pos.r_right)] = None 
      else:
        # it's a gate
        d.l.room_at(self.target_pos).tiles[self.target_pos.down][self.target_pos.right] = Tile(T_FLOOR,locked_door.wall_type,locked_door.floor_type)
        d.room_network.nodes[(self.target_pos.level,self.target_pos.r_down,self.target_pos.r_right)] = None 
      n = Notification(self.entity.pos)
      n.set_message(self.entity.name+" has picked the lock!",0)
      n.set_sound("door_unlocking")
      n.play(d)    
      display.redraw_tile(d,self.target_pos,None)
      display.blit_map_to_screen()
      self.expend_energy(3)
    elif self.type == A_JAMLOCK:
      locked_door = d.l.tile_at(self.target_pos)
      locked_door.jammed = 1
      if locked_door.to_pos is not None:
        other_side = d.l.tile_at(locked_door.to_pos)
        other_side.jammed = 1
      n = Notification(self.entity.pos)
      n.set_message("Darn! You screwed up... The lock is jammed. Now you will not be able to open it until you find the key.",0)
      n.set_sound("cancel1")
      n.play(d)    
      self.expend_energy(3)
    elif self.type == A_FIREBOLT or self.type == A_LIGHTNING_BOLT:
      if not d.l.valid(self.target_pos) or not d.get_occupant(self.target_pos)==self.target:
        return 
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_CAST,30,self.entity,1,1)
      fire_resistance_helps = 0
      if self.type==A_FIREBOLT:
        n.set_message(self.entity.name+" zaps " + self.target.name + " with a firebolt!",0)
        n.set_flash(self.target_pos,display.firebolt,0.2)        
        n.set_sound("bolt01")
        mean_damage = 20
        fire_resistance_helps = 1
      elif self.type==A_LIGHTNING_BOLT:
        n.set_message(self.entity.name+" zaps " + self.target.name + " with a bolt of lightning!",0)
        n.set_flash(self.target_pos,display.lightning,0.2)
        n.set_sound("bolt10")      
        mean_damage = 30    
        if self.target.is_character:
          mean_damage = mean_damage * 2 / 3
      n.play(d)      
      self.expend_energy(1)
      damage = random_positive_int_around(mean_damage,0.5)
      self.target.take_damage(d,damage,magic_resistance_helps=1,fire_resistance_helps=fire_resistance_helps)
      if self.type==A_LIGHTNING_BOLT:
        try_for_knockback(d,self.target,self.entity.pos)
    elif self.type == A_CONFUSION or self.type == A_ADMONISHMENT or self.type == A_FEAR or self.type == A_STEAL_ITEM_SPELL:
      if not d.l.valid(self.target_pos) or not d.get_occupant(self.target_pos)==self.target:
        return 
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_CAST,10,self.entity,1,1)
      if self.type==A_CONFUSION:
        difficulty = 10
        n.set_message(self.entity.name+" zaps " + self.target.name + " with a confusing illusion!",0)
        n.set_flash(self.target_pos,display.confuse,0.2)
        n.set_sound("confuse")  
      elif self.type==A_ADMONISHMENT:
        difficulty = 15
        n.set_message(self.entity.name+" preaches a harsh sermon against " + self.target.name + "!",0)
        n.set_flash(self.target_pos,display.bible,0.5)
        n.set_sound("gong")         
      elif self.type==A_FEAR:
        difficulty = 12
        n.set_noise(1,SD_SCREECH,30,self.entity,1,1)
        n.set_message(self.entity.name+" lets out a terrifying screech at " + self.target.name + "!",0)
        n.set_sound("084-Monster06")  
      elif self.type==A_STEAL_ITEM_SPELL:
        difficulty = 15
        n.set_message(self.entity.name+" casts a strange spell...",0)
        n.set_sound("steal")  
      n.play(d)  
      if self.type==A_FEAR:
        wait(800)
      self.expend_energy(1)
      if self.target.make_magic_resistance_check(difficulty) or self.target.info().immune_to_enchantments:
        n = Notification(self.entity.pos)
        n.set_message(self.target.name+" resists the spell!",0)
        n.set_sound("cancel1")
        n.play(d)
      else:  
        if self.type == A_CONFUSION:
          self.target.add_condition(d,C_CONFUSED,2,6)
        elif self.type == A_ADMONISHMENT or self.type == A_FEAR:
          self.target.make_run_away(d)
        elif self.type==A_STEAL_ITEM_SPELL:
          options = []
          for i in range(len(self.target.items)):
            if self.target.items[i] is not None:
              options.append(i)
          if len(options)>0:
            item_num = random.choice(options)
            item_name = self.target.items[item_num].info().name
            d.l.tile_at(self.entity.pos).object = self.target.items[item_num]
            self.target.items[item_num] = None
            self.target.stats.recalc(d)
            n = Notification(self.target.pos)
            n.set_message("Your " + item_name + " has vanished!",0)       
            n.play(d)
    elif self.type == A_ALL_OUT_DEFENSE:
        n = Notification(self.entity.pos)
        n.set_flash(self.target_pos,display.defend,0.2)
        n.set_message(self.entity.name+" goes on the defensive!",0)
        n.play(d)
        move_cost = self.entity.info().move_cost
        self.expend_energy(move_cost)
        self.entity.add_condition(d,C_DEFENDING,1,1)
    elif self.type == A_SNEAK:
        n = Notification(self.entity.pos)
        n.set_message(self.entity.name+" starts sneaking!",0)
        n.play(d)
        self.expend_energy(0.3)
        self.entity.add_condition(d,C_SNEAKING,1e6,0) # lasts until dispelled
        self.entity.stats.recalc(d)
        display.redraw_tile(d,self.entity.pos,self.entity)
        display.show_glyphs(d)
        display.blit_map_to_screen()
    elif self.type == A_DODGE:
        n = Notification(self.entity.pos)
        n.set_flash(self.entity.pos,display.darkvortex,0.4)
        n.set_message(self.entity.name+" is ready to dodge attacks!",0)
        n.set_sound('064-Swing03')
        n.play(d)
        self.expend_energy(0.3)
        self.entity.add_condition(d,C_DODGING,10,10)
    elif self.type == A_INVISIBILITY:
        n = Notification(self.entity.pos)
        n.set_message(self.entity.name+" vanishes!",0)
        n.set_sound('dark2')
        n.play(d)
        self.expend_energy(1)
        self.entity.add_condition(d,C_INVISIBLE,10,10)
    elif self.type == A_USE_PORTAL:
      if not d.l.valid(self.target_pos) or not adjacent_inc_through_portal(d,self.entity.pos,self.target_pos) or not d.l.tile_at(self.target_pos).info().walkable(self.entity):
        return
      if self.entity.is_character:
        d.clear_glyph_stack()
      self.expend_energy(1)
      self.target = d.get_occupant(self.target_pos)
      self.entity.move(d,self.target_pos,self.target)
    elif self.type == A_USE_EXIT:
      if self.entity.is_character:
        if not d.l.tile_at(self.entity.pos).info().is_exit:
          return
        else:
          sound.play("levelup")
          wait(ms=500)
          self.entity.escape(d)
          d.escaped_to = d.l.tile_at(self.entity.pos).exit_to
      else:
        if not d.l.tile_at(self.entity.pos).info().is_monster_exit:
          return
        else:
          n = Notification(self.entity.pos)
          n.set_message(self.entity.name + ' escapes!',0)
          n.play(d)
          self.entity.escape(d)
    elif self.type == A_HEALING:
      if not self.target.is_character or self.target.hp < self.target.heal_max_hp:
        if self.target.is_character:
          self.target.hp = min(self.target.hp+random.randint(3,8),self.target.heal_max_hp)
        else:
          self.target.hp = min(self.target.hp+random.randint(3,5),self.target.original_hp)
        n = Notification(self.target.pos)
        n.set_noise(1,SD_CAST,10,self.target,1,1)
        if self.entity==self.target:
          n.set_message(self.target.name+" is healed!",0)
        else:
          n.set_message(self.entity.name+" casts a healing spell on " + self.target.name + "!",0)
        if not self.target.is_character:
          n.set_flash(self.target_pos,display.yellowstars,0.2)
        n.set_sound("fanfare2")
        n.play(d)
      else:
        n = Notification(self.target.pos)
        n.set_noise(1,SD_CAST,10,self.target,1,1)
        n.set_message(self.target.name+" cannot heal any more right now...",0)
        n.set_sound("knock")
        n.play(d)
      self.expend_energy(1)  
    elif self.type == A_BLESSING:
      # target is an [entity]
      target_posses = []
      duration = random.randint(10,20)
      for individual_target in self.target:
        if not individual_target.no_longer_here() and not individual_target.has_condition(C_BLESSED):
          target_posses.append(individual_target.pos)
          individual_target.add_condition(d,C_BLESSED,duration,0)
      n = Notification(self.entity.pos)
      n.set_noise(1,SD_CAST,10,self.entity,1,1)
      n.set_message(self.entity.name+" casts a heathen blessing!",0)
      n.set_sound("decrease")
      n.set_multiflash(target_posses,display.badbook,0.7)
      n.play(d)
      self.expend_energy(1)  
      for individual_target in self.target:
         display.redraw_tile(d,individual_target.pos,individual_target)
      display.show_glyphs(d)
      display.blit_map_to_screen()       
    elif self.type == A_DETECT_ENEMIES:
      display.draw_minimap(d,detect_enemies=1)
      no_input = 1
      while (no_input):
          events = pygame.event.get()
          for event in events:
              if event.type==KEYDOWN or event.type==MOUSEBUTTONUP:
                  no_input = 0
      display.draw_map(d)
      display.show_glyphs(d)
      display.blit_map_to_screen()       
    elif self.type == A_CLAIRVOYANCE:
      display.send_message("You concentrate...")
      display.move_view_to(d,self.target_pos,fogged=1)
      display.send_message("Click to continue...")
      no_input = 1
      while (no_input):
          events = pygame.event.get()
          for event in events:
              if event.type==KEYDOWN or event.type==MOUSEBUTTONUP:
                  no_input = 0
      display.move_view_to(d,self.entity.pos) 
      display.send_message("The vision fades...")
    elif self.type == A_FIREBREATH or self.type == A_BIG_FIREBREATH:
      if not d.l.valid(self.target_pos):
        return 
      if self.type == A_BIG_FIREBREATH:
        mean_damage = 10
        brange = 8
      else:
        mean_damage = 6
        brange = 5
      fire_resistance_helps = 1  
      self.expend_energy(1)
      damage = random_positive_int_around(mean_damage,0.5)
      angle = right_down_to_angle([self.target_pos.right-self.entity.pos.right,self.target_pos.down-self.entity.pos.down])
      if angle is None:
        print "None angle!"
        print self.target_pos
        print self.entity.pos
        raise SystemError
      spaces_affected = get_cone(d,origin=self.entity.pos,angle=angle,length=brange,halfspread=20)
      for pos in spaces_affected:
        occupant = d.get_occupant(pos)
        if occupant is None:
          if random.random() < 0.2:
            if not d.l.is_temporary_fire(pos):
              d.l.add_temporary_fire(pos.copy())
              display.redraw_tile(d,pos,None)
      n = Notification(self.entity.pos)
      n.set_sound('119-Fire03')
      n.set_noise(1,SD_WHOOSH,22,self.entity,1,1)
      n.set_message(self.entity.name+" breathes fire!",0)
      n.set_multiflash(spaces_affected,display.fire,0.1)        
      n.play(d)   
      for pos in spaces_affected:
        occupant = d.get_occupant(pos)
        if occupant is not None:
          trivial = occupant.take_damage(d,damage,magic_resistance_helps=1,fire_resistance_helps=fire_resistance_helps)      
          if trivial:
            n = Notification(self.entity.pos)
            n.set_message(occupant.info().name + " shrugs it off.",0)
            n.play(d)
    elif self.type==A_WINDBLAST:
      if not d.l.valid(self.target_pos):
        return 
      mean_damage = 5
      brange = MAX_MAP_DIM
      self.expend_energy(1)
      damage = random_positive_int_around(mean_damage,0.5)
      angle = right_down_to_angle([self.target_pos.right-self.entity.pos.right,self.target_pos.down-self.entity.pos.down])
      if angle is None:
        print "None angle!"
        print self.target_pos
        print self.entity.pos
        raise SystemError
      spaces_affected = get_cone(d,origin=self.entity.pos,angle=angle,length=brange,halfspread=10,from_end=1)
      n = Notification(self.entity.pos)
      n.set_sound('134-Wind03')
      n.set_noise(1,SD_WHOOSH,22,self.entity,1,1)
      n.set_message(self.entity.name+" invokes a hurricane-force wind!",0)    
      n.play(d)   
      for pos in spaces_affected:
        occupant = d.get_occupant(pos)
        if occupant is not None:
          occupant.take_damage(d,damage,magic_resistance_helps=1,fire_resistance_helps=0)      
          for i in [0,1,2,3,4]:
            try_for_knockback(d,occupant,self.entity.pos)
    elif self.type == A_NOTHING:
        self.expend_energy(1)
        return
    else:
      return
    if not self.entity.no_longer_here() and self.entity.has_condition(C_SNEAKING):
      if not self.type in [A_WALK,A_USE_PORTAL,A_USE_EXIT,A_JUMP,A_SNEAK,A_DODGE,A_NOTHING]:
        self.entity.remove_condition_if_present(d,C_SNEAKING)
        self.entity.stats.recalc(d)
        display.redraw_tile(d,self.entity.pos,self.entity)
        display.show_glyphs(d)
        display.blit_map_to_screen()
        wait(ms=200)
        
def make_basic_move_action(d,entity,new_pos):
  # pass new_pos=None iff taking an exit
  if new_pos is None:
    return Action(entity,A_USE_EXIT,None,None)
  move_code, target = entity.can_move(d,new_pos)
  if move_code == M_HIT:
    return Action(entity,A_HIT,new_pos,target) # May be able to hit that pos even if unable to walk there (e.g. hitting a flying enemy over water)
  if move_code==M_FAIL:
    return Action(entity,A_NOTHING,None,None)
  if not entity.pos.same_room(new_pos):
    return Action(entity,A_USE_PORTAL,new_pos,target)
  return Action(entity,A_WALK,new_pos,target)

def get_cone(d,origin,angle,length,halfspread,from_end=0):
  # return a [pos]
  # angle and halfspread are in degrees
  # origin is not included
  result = []
  temp_pos = origin.copy()
  entity = d.get_occupant(origin)
  for i in range(1,length+1):
    in_this_shell = 0
    for down_right in d.shells[i]:
      temp_pos.down = origin.down + down_right[0]
      temp_pos.right = origin.right + down_right[1]
      if down_right[0]*down_right[0]+down_right[1]*down_right[1] > length*length:
        continue
      if not d.l.valid(temp_pos):
        continue
      if not d.l.tile_at(temp_pos).info().shootable:
        continue
      this_angle = right_down_to_angle([down_right[1],down_right[0]])
      if abs(this_angle-angle)>halfspread and abs(this_angle-angle+360)>halfspread and abs(this_angle-angle-360)>halfspread:
        continue
      if not d.line_of_shot(entity,temp_pos,[],exclude_all_but_small=0):
        continue
      in_this_shell = 1
      result.append(temp_pos.copy())
    if not in_this_shell:
      break
  if from_end:
    result.reverse()
  return result

def try_for_knockback(d,entity,hit_from):
  # with prob 1 if we get this far and the conditions are satisfied
  if not entity.is_dead() and not entity.info().size==SZ_LARGE and not entity.info().immobile and not entity.info().moves_through_flame_only and not entity.info().moves_through_water_only:
    dir_knocked = hit_from.direction_towards(entity.pos)
    if dir_knocked is not None:
      pos_knocked_to = get_pos_in_dir(d,entity.pos,dir_knocked)
      if d.l.valid(pos_knocked_to) and d.l.tile_at(pos_knocked_to).info().walkable(entity):
        occupant = d.get_occupant(pos_knocked_to)
        if occupant is None:
          n = Notification(entity.pos)
          n.set_message(entity.name + " is knocked back!",0)
          n.play(d)
          entity.move(d,pos_knocked_to,None)
          if not entity.is_character:
            entity.energy = entity.energy - 1

# do some psyco bindings 
psyco.bind(Action.resolve)
psyco.bind(Action.expend_energy)
psyco.bind(get_cone)